﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;
using System.Globalization;
using System.ComponentModel;

namespace SwiftMVVM.ViewModel
{
    /// <summary>
    /// NotifyProxyTypeManager dynamically creates a type that fires off the PropertyChanged/ing
    /// events for objects that implement INotifyPropertyChanged/ing
    /// </summary>
    public static class NotifyProxyTypeManager
    {
        /// <summary>
        /// The module builder we'll use to emit the proxy class at runtime
        /// </summary>
        private static readonly ModuleBuilder moduleBuilder =
            AppDomain.CurrentDomain.DefineDynamicAssembly(
            new AssemblyName("DynamicProxyAssembly"), AssemblyBuilderAccess.Run).
            DefineDynamicModule("DynamicProxyModule");

        /// <summary>
        /// Dictionary of wrapped types
        /// </summary>
        private static readonly Dictionary<Type, Type> typeWrapperCache = new Dictionary<Type, Type>();

        /// <summary>
        /// Locking object
        /// </summary>
        private static readonly object typeWrapperLock = new object();

        /// <summary>
        /// Creates a proxy for the type to wrap it's property setters
        /// that are marked with NotificationPropertyAttribute
        /// </summary>
        /// <typeparam name="T">The type to create</typeparam>
        /// <returns>A class that derives from T with overridden property 
        /// setters for properties marked with NotificationPropertyAttribute</returns>
        public static T Allocate<T>() where T : class, INotifyPropertyChanged
        {
            return (T)Activator.CreateInstance(GetCachedType(typeof(T)));
        }

        /// <summary>
        /// Creates a proxy for the type to wrap it's property setters
        /// that are marked with NotificationPropertyAttribute
        /// </summary>
        /// <typeparam name="T">The type to create</typeparam>
        /// <param name="constructorParams">Constructor arguments</param>
        /// <returns>A class that derives from T with overridden property 
        /// setters for properties marked with NotificationPropertyAttribute</returns>
        public static T Allocate<T>(params object[] constructorParams) where T : class, INotifyPropertyChanged
        {
            return (T)Activator.CreateInstance(GetCachedType(typeof(T)),
               BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public,
               null, constructorParams, CultureInfo.CurrentCulture);
        }

        /// <summary>
        /// In some cases, such as the creation of many objects over and over
        /// It actually makes sense to get the allocator and store it for use later
        /// </summary>
        /// <typeparam name="T">The type to create</typeparam>
        /// <param name="args">Constructor arguments</param>
        /// <returns>A compiled delegate that can be saved and reused to create the object</returns>
        public static Func<object[], object> GetAllocator<T>(params object[] args) where T : class, INotifyPropertyChanged
        {
            return TypeFactory.GetAllocator(GetCachedType(typeof(T)), args);
        }

        /// <summary>
        /// In some cases, such as the creation of many objects over and over
        /// It actually makes sense to get the allocator and store it for use later
        /// The difference here is that instead of inferring the ctor argument
        /// types, you pass them directly
        /// </summary>
        /// <typeparam name="T">The type to create</typeparam>
        /// <param name="args">Constructor arguments</param>
        /// <returns>A compiled delegate that can be saved and reused to create the object</returns>
        public static Func<object[], object> GetAllocatorExact<T>(params Type[] argumentTypes) where T : class, INotifyPropertyChanged
        {
            return TypeFactory.GetAllocatorExact(GetCachedType(typeof(T)), argumentTypes);
        }

        /// <summary>
        /// Returns the dynamically created type
        /// </summary>
        /// <param name="typeToWrap">The type we're going to wrap</param>
        /// <returns>A type that derives from typeToWrap which overrides all of the
        /// property setters marked with NotificationProperty attribute</returns>
        private static Type GetCachedType(Type typeToWrap)
        {
            return typeWrapperCache.GetOrAdd(typeWrapperLock, typeToWrap, type =>
            {
                //If the type isnt public our assembly will not be able to derive from it
                if (!type.IsPublic) { throw new InvalidOperationException("wrapped type must be public"); }

                //We prefer the type to be abstract because we want to make sure that it's only
                //creatable thru the proxy, otherwise it could be created without it's property setter override
                if (!type.IsAbstract) { throw new InvalidOperationException("wrapped type must be abstract"); }

                //The object must implement INotifyPropertyChanged
                if (!typeof(INotifyPropertyChanged).IsAssignableFrom(type))
                { throw new InvalidOperationException("wrapped type must implement INotifyPropertyChanged"); }

                var typeBuilder = moduleBuilder.DefineType("SwiftMVVM." + type.Name + "!PropChangeProxy",
                        TypeAttributes.Class | TypeAttributes.Public, type);

                //Find all the properties marked NotificationPropertyAttribute
                foreach (var propertyInfo in type.GetProperties(
                   BindingFlags.GetProperty | BindingFlags.SetProperty |
                   BindingFlags.Public | BindingFlags.NonPublic |
                   BindingFlags.Instance)
                   .Where(pi => pi.GetCustomAttributes(typeof(NotificationPropertyAttribute), true).Any()))
                {
                    //The properties must be writable/readable
                    if (!propertyInfo.CanRead || !propertyInfo.CanWrite)
                    {
                        throw new InvalidOperationException(
                            string.Format("Property {0}.{1} doesn't have a getter and setter",
                            type.FullName, propertyInfo.Name));
                    }

                    //Make sure we have a public setter, and that it's virtual
                    var methodInfo = propertyInfo.GetSetMethod(true);
                    if (methodInfo == null)
                    {
                        throw new InvalidOperationException(
                            string.Format("Property {0}.{1} does not have a public setter",
                            type.FullName, propertyInfo.Name));
                    }

                    if (!methodInfo.IsVirtual)
                    {
                        throw new InvalidOperationException(
                            string.Format("Property {0}.{1} is not virtual", type.FullName, propertyInfo.Name));
                    }

                    if (methodInfo.IsAbstract)
                    {
                        throw new InvalidOperationException(
                            string.Format("Property {0}.{1} must be marked virtual and not abstract", type.FullName, propertyInfo.Name));
                    }

                    OverridePropertySet(typeBuilder, propertyInfo);
                }

                BuildConstructors(type, typeBuilder);

                return typeBuilder.CreateType();
            });
        }

        /// <summary>
        /// Overrides the property setter in order to fire off the property changed and property
        /// changing events if the property changes
        /// </summary>
        /// <param name="typeBuilder"></param>
        /// <param name="propertyInfo"></param>
        private static void OverridePropertySet(TypeBuilder typeBuilder, PropertyInfo propertyInfo)
        {
            //Little bit of setup, we're going to need to call the base Get/Set property methods
            //Then if we detect a change we're going to need to call 
            //Get/Set base property/Compare/RaisePropertyChanged (and optionally RaisePropertyChanging)
            var getPropertyMethodInfo = propertyInfo.GetGetMethod();
            if (getPropertyMethodInfo == null)
            {
                throw new InvalidOperationException(
                            string.Format("Property {0}.{1} could not access Get Method",
                            propertyInfo.DeclaringType.FullName, propertyInfo.Name));
            }

            var setPropertyMethodInfo = propertyInfo.GetSetMethod() ?? propertyInfo.GetSetMethod(true);
            if (setPropertyMethodInfo == null)
            {
                throw new InvalidOperationException(
                            string.Format("Property {0}.{1} could not access Set Method",
                            propertyInfo.DeclaringType.FullName, propertyInfo.Name));
            }

            var compareMethodInfo = typeof(NotifyProxyTypeManager).GetMethods(
                  BindingFlags.Static | BindingFlags.Public).Where(mi => mi.ToString()
                      == "Boolean Compare[T](T, T)").First();

            if (compareMethodInfo == null)
            {
                throw new InvalidOperationException("Compare Method Inaccessible on PropertySetNotificationProxy");
            }

            var raisePropertyChangedMethodInfo = propertyInfo.DeclaringType.GetMethods(
                BindingFlags.Instance | BindingFlags.NonPublic).Where(mi => mi.ToString()
                    == "Void RaisePropertyChanged(System.String)").FirstOrDefault();

            if (raisePropertyChangedMethodInfo == null)
            {
                throw new InvalidOperationException(
                    string.Format("Could not locate Void RaisePropertyChanged(System.String) on {0}",
                    propertyInfo.DeclaringType.FullName));
            }

            //Optionally support INotifyPropertyChanging
            MethodInfo raisePropertyChangingMethodInfo = null;
            if (typeof(INotifyPropertyChanging).IsAssignableFrom(propertyInfo.DeclaringType))
            {
                raisePropertyChangingMethodInfo = propertyInfo.DeclaringType.GetMethods(
                BindingFlags.Instance | BindingFlags.NonPublic).Where(mi => mi.ToString()
                    == "Void RaisePropertyChanging(System.String)").FirstOrDefault();
            }

            //Create a MethodBuilder to override the property setter
            var methodBuilder = typeBuilder.DefineMethod(propertyInfo.GetSetMethod(true).Name,
                MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.SpecialName,
                CallingConventions.Standard,
                typeof(void),
                new Type[] { propertyInfo.PropertyType });

            var ilGenerator = methodBuilder.GetILGenerator();

            //We need a local boolean to store the result of the value compare 
            //we're going to be doing against the current and new property value
            ilGenerator.DeclareLocal(typeof(bool));

            //This label is marked at the end of the override function
            //If the property set does not cause a change in the property value (ie. Obj.Prop = Obj.Prop)
            //then we just exit
            var exitFunctionLabel = ilGenerator.DefineLabel();

            //TODO find out why we need/may want to generate a NOP
            ilGenerator.Emit(OpCodes.Nop);

            //Call the property get function on the base to get our current value
            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Callvirt, getPropertyMethodInfo);
            ilGenerator.Emit(OpCodes.Ldarg_1);

            //Compare our current and our potential new value using 
            //PropertySetNotificationProxy.Compare<T> will return true if a difference is found (should change this)
            ilGenerator.Emit(OpCodes.Call, compareMethodInfo.MakeGenericMethod(new Type[] { propertyInfo.PropertyType }));

            //Based on the return value of PropertySetNotificationProxy.Compare<T>
            //We're either going to fire Changing/Set/Changed, or we're going to exit
            //If compare returns true (ie. it found the values are the same we jump to the end of this method)
            ilGenerator.Emit(OpCodes.Ldc_I4_0);
            ilGenerator.Emit(OpCodes.Ceq);
            ilGenerator.Emit(OpCodes.Stloc_0);
            ilGenerator.Emit(OpCodes.Ldloc_0);
            ilGenerator.Emit(OpCodes.Brtrue_S, exitFunctionLabel);

            //This object supports INotifyPropertyChanging
            //Fire off RaisePropertyChanging to alert consumers
            //that this property is about to change
            if (raisePropertyChangingMethodInfo != null)
            {
                ilGenerator.Emit(OpCodes.Ldarg_0);
                ilGenerator.Emit(OpCodes.Ldstr, propertyInfo.Name);
                ilGenerator.Emit(OpCodes.Callvirt, raisePropertyChangingMethodInfo);
            }

            //Call off the base property setter
            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Ldarg_1);
            ilGenerator.Emit(OpCodes.Call, setPropertyMethodInfo);

            //Fire off RaisePropertyChanged to alert consumers
            //that this property has just changed
            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Ldstr, propertyInfo.Name);
            ilGenerator.Emit(OpCodes.Callvirt, raisePropertyChangedMethodInfo);

            //We're done
            ilGenerator.MarkLabel(exitFunctionLabel);
            ilGenerator.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Copies the constructors from the base class into the generated type
        /// </summary>
        /// <param name="typeToWrap">The type w're going to wrap</param>
        /// <param name="type"></param>
        private static void BuildConstructors(Type typeToWrap, TypeBuilder type)
        {
            foreach (ConstructorInfo baseConstructor in typeToWrap.GetConstructors())
            {
                Type[] constructorParameterTypes = baseConstructor.GetParameters().Select(pi => pi.ParameterType).ToArray();
                string[] constructorParameterNames = baseConstructor.GetParameters().Select(pi => pi.Name).ToArray();

                ConstructorBuilder constructor = type.DefineConstructor(MethodAttributes.Public |
                    MethodAttributes.HideBySig, CallingConventions.Standard, constructorParameterTypes);

                for (int parameterCount = 0; parameterCount != constructorParameterTypes.Length; parameterCount++)
                {
                    constructor.DefineParameter(parameterCount + 1, ParameterAttributes.None, constructorParameterNames[parameterCount]);
                }

                ILGenerator ilGenerator = constructor.GetILGenerator();
                ilGenerator.Emit(OpCodes.Ldarg_0);

                for (int parameterCount = 0; parameterCount < constructorParameterTypes.Length; parameterCount++)
                {
                    switch (parameterCount)
                    {
                        case 0:
                            ilGenerator.Emit(OpCodes.Ldarg_1);
                            break;
                        case 1:
                            ilGenerator.Emit(OpCodes.Ldarg_2);
                            break;
                        case 2:
                            ilGenerator.Emit(OpCodes.Ldarg_3);
                            break;
                        default:
                            ilGenerator.Emit(OpCodes.Ldarg_S, (byte)(parameterCount + 1));
                            break;
                    }
                }

                ilGenerator.Emit(OpCodes.Call, baseConstructor);
                ilGenerator.Emit(OpCodes.Ret);
            }
        }



        /// <summary>
        /// Compares the values to see if they match
        /// </summary>
        /// <typeparam name="T">The type we're comparing</typeparam>
        /// <param name="currentValue">Current Value</param>
        /// <param name="value">New Value</param>
        /// <returns>True if the values are different, false otherwise</returns>
        public static bool Compare<T>(T currentValue, T value)
        {

            //Let's determine if the value has changed
            if (currentValue == null && value == null)
            {
                return false;
            }

            //We can only do a detailed compare if
            //at least one of the values implements IComparable
            //Otherwise, we just fire off property changed
            if (currentValue as IEquatable<T> != null ||
                value as IEquatable<T> != null)
            {
                if (EqualityComparer<T>.Default.Equals(currentValue, value))
                {
                    //No change between the new and old value, do not go any further
                    return false;
                }
            }
            else
            {
                IComparable valueComparer = value as IComparable;
                if (valueComparer != null && valueComparer.CompareTo(currentValue) == 0)
                {
                    return false;
                }
            }
            return true;
        }

    }
}



